/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package pl.tewi.jeeunit.controller;

import com.sun.appserv.security.ProgrammaticLogin;
import com.sun.enterprise.security.auth.realm.file.FileRealm;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import org.glassfish.embeddable.CommandResult;
import org.glassfish.embeddable.CommandRunner;
import org.glassfish.embeddable.Deployer;
import org.glassfish.embeddable.GlassFish;
import org.glassfish.embeddable.GlassFishException;
import org.glassfish.embeddable.GlassFishProperties;
import org.glassfish.embeddable.GlassFishRuntime;
import org.glassfish.embeddable.archive.ScatteredArchive;
import org.glassfish.embeddable.archive.ScatteredEnterpriseArchive;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.tewi.jeeunit.controllers.ApplicationAssembler;
import pl.tewi.jeeunit.config.CopyServerConfiguration;
import pl.tewi.jeeunit.config.ExtractServerConfiguration;
import pl.tewi.jeeunit.config.GlassfishApplicationAssembler;
import pl.tewi.jeeunit.config.ServerConfigurationStrategy;
import pl.tewi.jeeunit.contexts.ApplicationServerContext;
import pl.tewi.jeeunit.contexts.FrameworkFieldInjector;
import pl.tewi.jeeunit.controllers.ServerController;
import pl.tewi.jeeunit.exceptions.ApplicationDeploymentException;
import pl.tewi.jeeunit.exceptions.AuthenticationException;
import pl.tewi.jeeunit.exceptions.EmbeddedServerException;
import pl.tewi.jeeunit.exceptions.GeneralFrameworkException;
import pl.tewi.jeeunit.exceptions.NetworkLookupException;
import pl.tewi.jeeunit.exceptions.PrepareConfigurationException;
import pl.tewi.jeeunit.types.ApplicationDesc;
import pl.tewi.jeeunit.utils.FileUtils;

/**
 * Klasa sterująca i komunikująca się z serweram aplikacyjnym Glassfish
 * @author Marcin Kwapisz
 */
public class GlassfishServerController implements ServerController {

    /**
     * Logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(GlassfishServerController.class);
    /**
     * GlassFishRuntime jest singletonem który przy ponownym bootstrap rzuca wyjątek
     */
    private GlassFishRuntime runtime;
    /**
     * Instancja serwera aplikacyjnego Glassfish
     */
    private GlassFish server = null;
    /**
     * Kontekst serwera aplikacyjnego
     */
    private final ApplicationServerContext applicationServerContext;

    /**
     * Tworzy kontroler serwera aplikacyjnego glassfish na podstawie przekazanego kontekstu
     * @param applicationServerContext kontekst serwera aplikacyjnego {@link ApplicationServerContext}
     */
    public GlassfishServerController(final ApplicationServerContext applicationServerContext) {
        this.applicationServerContext = applicationServerContext;
    }

    /**
     * Uruchamia serwer glassfish w trybie wbudowanym
     * Embedded Glassfish uwzględnia ścieżkę w której projekt się znajduje,
     * dlatego ścieżki muszą być względem projektu
     * @throws GeneralFrameworkException błąd uruchamiania serwera aplikacyjnego
     */
    @Override
    public void startEmbeddedServer() throws GeneralFrameworkException {
        
        LOGGER.debug(":Starting GF Server");
        GlassFishProperties properties = new GlassFishProperties();
        try {
            String instanceRootPath = applicationServerContext.getInstanceRootPath();
            properties.setInstanceRoot(instanceRootPath);
            properties.setConfigFileReadOnly(false);
            LOGGER.debug(":Preparing GF Server configuration files");
            prepareServerConfigurationFiles(instanceRootPath);
            prepareServerPortsConfiguration();
            //zaisncjalizuj tylko raz
            runtime = GlassFishRuntime.bootstrap();
            server = runtime.newGlassFish(properties);
            server.start();
            LOGGER.debug(":GF Embedded Domain {} started", applicationServerContext.getInstanceName());
        } catch (IOException ex) {
            throw new GeneralFrameworkException(ex);
        } catch (GlassFishException ex) {
            throw new EmbeddedServerException(EmbeddedServerException.CANNOT_START, ex);
        }
    }

    /**
     * Przygotowanie plików konfiguracyjnych domeny serwera glassfish i plików schematu
     * @param instanceRootPath ścieżka do instancji serwera aplikacyjnego w której zostanie umieszczona jego konfiguracja
     * @throws PrepareConfigurationException błąd tworzenia bazowej konfiguracji dla serwera aplikacyjnego
     */
    private void prepareServerConfigurationFiles(String instanceRootPath) throws PrepareConfigurationException {
        //określ lokalizację pliku jar bądź katalogu zawierajacego tą klasę
        //znajdują się tam pliki konfiguracji domeny i schematy dtd
        String sourcePath = GlassfishServerController.class.getProtectionDomain().getCodeSource().getLocation().getPath();
        File sourceFile = new File(sourcePath);

        //określ czy to jest katalog czy też archiwum jar
        ServerConfigurationStrategy serverConfiguration = null;
        try {
            if (sourceFile.isDirectory()) {
                LOGGER.info("Copying server configuration files from directory");
                //konfiguracja projektu pozwala skorzystać z plików już rozpakowanych
                serverConfiguration = new CopyServerConfiguration();
                
            } else {
                //rozpakuj z archiwum
                LOGGER.info("Extracting server configuration files from jar");
                //wypakuj pliki znadjujące się w tych katalogach
                serverConfiguration = new ExtractServerConfiguration();
            }

//            serverConfiguration.prepareConfiguration(sourceFile, "dtds", instanceRootPath + "/lib/");
            serverConfiguration.prepareConfiguration(sourceFile, "config", instanceRootPath);
        } catch (IOException ex) {
            LOGGER.error("", ex);
            throw new PrepareConfigurationException(ex);
        }
    }
    
    private void prepareServerPortsConfiguration() throws PrepareConfigurationException {
        if (applicationServerContext.getInstancePorts() != null) {
            System.setProperty("SERVER_ASADMIN_LISTENER_PORT", String.valueOf(applicationServerContext.getInstancePorts().getAdmin()));
            System.setProperty("SERVER_HTTP_LISTENER_PORT", String.valueOf(applicationServerContext.getInstancePorts().getHttp()));
            System.setProperty("SERVER_HTTP_SSL_LISTENER_PORT", String.valueOf(applicationServerContext.getInstancePorts().getHttps()));
            System.setProperty("SERVER_JMS_PROVIDER_PORT", String.valueOf(7676));
            System.setProperty("SERVER_IIOP_LISTENER_PORT", String.valueOf(applicationServerContext.getInstancePorts().getIiop()));
            System.setProperty("SERVER_IIOP_SSL_LISTENER_PORT", String.valueOf(applicationServerContext.getInstancePorts().getIiops()));
            System.setProperty("SERVER_IIOP_SSL_MUTUALAUTH_PORT", String.valueOf(3920));
            System.setProperty("SERVER_JMX_SYSTEM_CONNECTOR_PORT", String.valueOf(applicationServerContext.getInstancePorts().getJmx()));
            System.setProperty("SERVER_OSGI_SHELL_TELNET_PORT", String.valueOf(6666));
            System.setProperty("SERVER_JAVA_DEBUGGER_PORT", String.valueOf(9009));
        } else {
            throw new PrepareConfigurationException(PrepareConfigurationException.INSTANCE_PORT_NOT_SET);
        }
    }

    /**
     * zatrzymanie serwera aplikacyjnego glassfish
     * @throws GeneralFrameworkException bład podczas zatrzymywnia servera aplikacyjnego
     */
    @Override
    public void stopEmbeddedServer() throws GeneralFrameworkException {
        LOGGER.debug(":Stopping GF Server:");
        if (server != null) {
            try {
                server.stop();
                server.dispose();
                runtime.shutdown();
                runtime = null;
                LOGGER.debug(":GF domain {} stopped", applicationServerContext.getInstanceName());
            } catch (GlassFishException ex) {
                LOGGER.error("", ex);
                throw new EmbeddedServerException(EmbeddedServerException.CANNOT_STOP, ex);
            }
        }
    }

    /**
     * Instaluje na serwerze glassfish skonfigurowane dla niego aplikacje. 
     * @throws ApplicationDeploymentException w przypadku braku wsparcia dla danego typu aplikacji
     * @throws GeneralFrameworkException
     */
    @Override
    public void deployApplication() throws GeneralFrameworkException {
        try {
            //TODO dokończyć, narazie obsługuje tylko JAR
            Set<ApplicationDesc> applicationDescs = applicationServerContext.getApplications();
            ApplicationAssembler assembler = new GlassfishApplicationAssembler();
            //przy problemach z testem aplikacje mogą nie być odinstalowane
            //usun je wszystkie
            undeployAllApplications();
            Deployer deployer = server.getDeployer();
            String deployedApp;
            for (ApplicationDesc appDesc : applicationDescs) {
                //zignoruj aplikcaje nie przeznaczone dla tego serwera aplikacyjnego
                //generalnie taka sytuacja nie powinna sie zdarzyc
                if (appDesc.getTarget().equals(applicationServerContext.getInstanceName())) {
                    deployedApp = null;
                    Object assembledApp = assembler.build(appDesc);
                    LOGGER.info(":Trying to install application {}:", appDesc.getName());
                    //TODO dodać opcje podczas instalacji aplikacji
                    switch (appDesc.getApplicationType()) {
                        case JAR:
                            deployedApp = deployer.deploy(((ScatteredArchive) assembledApp).toURI());
                            break;
                        case WAR:
                            deployedApp = deployer.deploy(((ScatteredArchive) assembledApp).toURI());
                            break;
                        case EAR:
                            deployedApp = deployer.deploy(((ScatteredEnterpriseArchive) assembledApp).toURI());
                            break;
                        default:
                            throw new ApplicationDeploymentException(ApplicationDeploymentException.UNSUPPORTED_APP_TYPE);
                    }
                    if (deployedApp == null) {
                        LOGGER.error("Unsupported application type of name {}", appDesc.getName());
                        throw new ApplicationDeploymentException(ApplicationDeploymentException.UNSUPPORTED_APP_TYPE);
                    } else {
                        LOGGER.info(":Application {} installed:", deployedApp);
                    }
                } else {
                    LOGGER.debug("Application {} ignored for deployment on server {}", new Object[]{appDesc.getName(), applicationServerContext.getInstanceName()});
                }
            }
        } catch (IOException ex) {
            throw new GeneralFrameworkException(ex);
        } catch (GlassFishException ex) {
            throw new GeneralFrameworkException(ex);
        }
    }

    /**
     * Usuwa szystkie aplikacje zainstalowane na serwerze aplikacyjnym
     * @throws GlassFishException 
     */
    private void undeployAllApplications() throws GlassFishException {
        Collection<String> deployedApps = server.getDeployer().getDeployedApplications();
        Iterator<String> iterator = deployedApps.iterator();
        while (iterator.hasNext()) {
            server.getDeployer().undeploy(iterator.next(), "--droptables", "false");
        }
    }
    
    @Override
    public void deployApplicationResources() throws GeneralFrameworkException {
        ArrayList<String> resources = applicationServerContext.getResourceFiles();
        if (!resources.isEmpty()) {
            LOGGER.debug("Deploying application resources");
            try {
                CommandRunner runner = server.getCommandRunner();
                for (String resource : resources) {
                    CommandResult result = runner.run("add-resources", resource);
                    if (result.getExitStatus() == CommandResult.ExitStatus.FAILURE) {
                        LOGGER.error(result.getOutput());
                        throw new GeneralFrameworkException(result.getFailureCause());
                    }
                }
            } catch (GlassFishException ex) {
                throw new GeneralFrameworkException(ex);
            }
        } else {
            LOGGER.debug("No resources configured");
        }
    }
    
    @Override
    public void configureSecuritySettings() throws GeneralFrameworkException {
        try {
            CommandRunner runner = server.getCommandRunner();
            runner.setTerse(true);
            prepareSecurityRealm(runner);
            prepareRealmUsers(runner);
        } catch (GlassFishException ex) {
            throw new GeneralFrameworkException(ex);
        } catch (IOException ex) {
            throw new GeneralFrameworkException(ex);
        }
    }

    /**
     * Przygotowuje realm bezpieczeństwa. Najpierw usuwa realm jeżeli istniał o takiej nazwie i jego plik.
     * Potem tworzy nowy realm i ustawia go jako domyślny na serwerze
     * @param runner
     * @throws GeneralFrameworkException
     * @throws IOException 
     */
    private void prepareSecurityRealm(CommandRunner runner) throws GeneralFrameworkException, IOException {
        String command;
        CommandResult result;
        //TODO usunąć realm, nie kasuje to pliku ???
        command = "delete-auth-realm";
        String realmFilePath = FileUtils.joinPaths(applicationServerContext.getInstanceRootPath(), "config");
        realmFilePath = FileUtils.joinPaths(realmFilePath, applicationServerContext.getSecurityRealmName());
        File realmFile = new File(realmFilePath);
        if (realmFile.exists()) {
            realmFile.delete();
        }
        result = runner.run(command, applicationServerContext.getSecurityRealmName());
        if (result.getExitStatus() == CommandResult.ExitStatus.FAILURE && !result.getOutput().contains("not found")) {
            throw new GeneralFrameworkException(result.getFailureCause());
        }
        // Utwórz testowy security realm
        //asadmin> create-auth-realm --classname com.sun.enterprise.security.auth.realm.file.FileRealm --property file=${com.sun.aas.instanceRoot}/config/filerealm:jaas-context=fileRealm fileRealm
        //czasamin nie tworzy pliku
        command = "create-auth-realm";
//        realmFile.createNewFile();
//        result = runner.run(command, "--classname", FileRealm.class.getCanonicalName(), "--property", "file="+ realmFile.getAbsolutePath() + ":jaas-context=fileRealm", applicationServerContext.getSecurityRealmName());
        //TODO koszmar tutaj spowodowany niewykonaniem polecenia tworzenia realmu (jeżeli tą metodę wywoła się jedna po drugiej)
//        String configRealmFilePath=realmFile.getPath().replace("\\", "/");
        result = runner.run(command, "--classname", FileRealm.class.getCanonicalName(), "--property", "file=${com.sun.aas.instanceRoot}/config/" + applicationServerContext.getSecurityRealmName() + ":jaas-context=fileRealm", applicationServerContext.getSecurityRealmName());
//        result = runner.run(command, "--classname", FileRealm.class.getCanonicalName(), "--property", "file=${com.sun.aas.instanceRoot}/config/" + applicationServerContext.getSecurityRealmName() + ":jaas-context=fileRealm", applicationServerContext.getSecurityRealmName());

        if (result.getExitStatus().equals(CommandResult.ExitStatus.FAILURE)) {
            LOGGER.error(result.getOutput());
            throw new GeneralFrameworkException(result.getFailureCause());
        }
        //Przełącz domyślny security realm na testowy
        //asadmin> set configs.config.server-config.security-service.default-realm=fileRealm
        command = "set";
        result = runner.run(command, "configs.config.server-config.security-service.default-realm=" + applicationServerContext.getSecurityRealmName());
        if (result.getExitStatus().equals(CommandResult.ExitStatus.FAILURE)) {
            throw new GeneralFrameworkException(result.getFailureCause());
        }
    }

    /**
     * Tworzy uzytkowników w realm-ie bezpieczeństwa
     * @param runner interfejs pozwalający wykonywać polecenia cli 
     * @throws IOException błąd tworzenia pliku z hasłem użytkownika
     * @throws GeneralFrameworkException błąd tworzenia realmu bezpieczeństwa
     */
    private void prepareRealmUsers(final CommandRunner runner) throws IOException, GeneralFrameworkException {
        String command;
        CommandResult result;
        //Utwórz użytkowników, koniecznie trzeba utworzyć plik w formacie AS_ADMIN_USERPASSWORD=user-password
        //asadmin> create-file-user --authrealmname file --groups testuser mk --passwordfile=c:/JavaDev/Java/appservers/glassfish-3.1.1/glassfish/domains/domain1/config/passwords.txt
        ArrayList<String> users = applicationServerContext.getUsers();
//        String passwordFilePath = FileUtils.joinPaths(applicationServerContext.getInstanceRootPath(), applicationServerContext.getSecurityRealmName() + ".txt");
        File userPasswordFile;
        command = "create-file-user";
        for (String user : users) {
            userPasswordFile = new File(applicationServerContext.getInstanceRootPath(), user);
            createUsersPasswordFile(userPasswordFile, user, user);
            result = runner.run(command, "--authrealmname", applicationServerContext.getSecurityRealmName(), "--passwordfile=" + userPasswordFile.getPath(), "--groups", user, user);
//            result = runner.run(command, "--authrealmname", "file", "--passwordfile=" + userPasswordFile.getPath(), "--groups", user, user);
            if (result.getExitStatus().equals(CommandResult.ExitStatus.FAILURE)) {
                LOGGER.error(result.getOutput());
                throw new GeneralFrameworkException(result.getFailureCause());
            }
        }
    }

    /**
     * Tworzy plik z listą haseł użytkowników
     * @param file plik w którym będzie znajdować się hasło użytkownika
     * @param user nazwa użytkownika
     * @param password hasło użytkownika do zapisania w pliku
     * @return
     * @throws IOException 
     */
    private void createUsersPasswordFile(File file, String user, String password) throws IOException {
        //usun zbędną konfigurację
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        PrintWriter printWriter = new PrintWriter(file);
        //AS_ADMIN_USERPASSWORD=password - UWAGA - niezgodne z gokumentacją w której napisano że format to AS_ADMIN_USERPASSWORD=user-password (działa z asadmin)
        printWriter.print("AS_ADMIN_USERPASSWORD=");
        printWriter.println(password);
        printWriter.flush();
        printWriter.close();
    }

    /**
     * Pobiera kontekst transakcji związany z danym wątkiem
     * @return pobiera interfejs transakcji użytkownika
     * @throws GeneralFrameworkException 
     */
    @Override
    public UserTransaction getCurrentTransaction() throws GeneralFrameworkException {
        try {
            UserTransaction tx = (UserTransaction) getInitialContext(null).lookup("java:comp/UserTransaction");
            return tx;
        } catch (NamingException ex) {
            throw new NetworkLookupException(NetworkLookupException.USER_TRANSACTION_LOOKUP, ex);
        }
    }

    /**
     * Wyszukuje zasób w zainstalowanych aplikacjach, lub na serwerze aplikacyjnym
     * i wstrzykuje go w pole testu opisane adnotacją
     * @param test obiekt klasy testowej
     * @param runAsUser uruchom test jako użytkownik
     * @throws GeneralFrameworkException 
     */
    @Override
    public void injectResources(final Object test, final String runAsUser) throws GeneralFrameworkException {
        InitialContext initialContext = getInitialContext(runAsUser);
        FrameworkFieldInjector injector = FrameworkFieldInjector.getFrameworkFieldInjector();
        injector.inject(test, initialContext, applicationServerContext);
    }

    /**
     * Pobiera kontekst do przeszukiwania JNDI serwera aplikacyjnego. 
     * @param runAsUser musi tutaj być z uwagi na różne zposoby uwierzytleniania
     * Dla niektórych serwerów aplikacyjnych odbywa się to poprzez parametry
     * przekazywane do {@link InitialContex}. W tej implementacji parametr nie 
     * jest wykorzystywany, patrz {@link GlassfishServerController#login(java.lang.String, java.lang.String) }
     * @return InitialContext do przeszukiwania JNDI serwera aplikacyjnego
     * @throws NetworkLookupException problem z utworzeniem InitialContext
     */
    private InitialContext getInitialContext(final String runAsUser) throws NetworkLookupException {
        try {
            Properties props = new Properties();
            //te parametry nie są akceptowalne w trybie embedded
//            props.setProperty("org.omg.CORBA.ORBInitialHost", applicationServerContext.getHostName());
//            props.setProperty("org.omg.CORBA.ORBInitialPort", Integer.toString(applicationServerContext.getInstancePorts().getIiop()));

            InitialContext ctx = new InitialContext(props);
            return ctx;
        } catch (NamingException ex) {
            throw new NetworkLookupException(NetworkLookupException.INITIAL_CONTEXT_CREATION, ex);
        }
    }

    /**
     * Uwierzytelnia użytkownika. Zakłożono, że nazawa użytkownika=hasło=grupa
     * <p>Konieczne jest 
     * @param user nazwa uzytkownika,
     * @param password hasło użytkownika skonfigurowane dla realmu bezpieczeństwa
     * podanego w {@link GlassfishServerController#applicationServerContext}
     * @throws GeneralFrameworkException 
     */
    @Override
    public void login(final String user, final String password) throws GeneralFrameworkException {
        ProgrammaticLogin pl = new ProgrammaticLogin();
        try {
            pl.login(user, password.toCharArray(), applicationServerContext.getSecurityRealmName(), true);
        } catch (Exception ex) {
            throw new AuthenticationException("User does not exists in security realm" + applicationServerContext.getSecurityRealmName(), ex);
        }
    }

    /**
     * Operacja dla GlassFish jest konieczna, gdyż po zalogowaniu wszystkie 
     * metody wykonane byłyby w konteście danego użytkownika.
     * @throws GeneralFrameworkException 
     */
    @Override
    public void logout() throws GeneralFrameworkException {
        ProgrammaticLogin pl = new ProgrammaticLogin();
        try {
            pl.logout(true);
        } catch (Exception ex) {
            throw new GeneralFrameworkException(ex);
        }
    }
}
